﻿using Devil;
using System.Collections;
using System.Collections.Generic;
using System.Text.RegularExpressions;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
using UnityEngine.Profiling;

namespace GameToolkit
{
#if UNITY_EDITOR
    public interface IInnerAssetBundle
    {
        bool IsActive { get; }
        void Activite();
        void Deactivite();
        string GetVariantAsset(string asset);
        //string GetAssetBundle(string asset);
        string[] GetAllAssets(string abName);
        bool HasAssetBundle(string abName);
    }
#endif

    public class InnerAssetsUtil : AssetsUtil, ICommandHandler
    {
        readonly static object _lock = new object();

        class AbHandler : IAssetPtr, ICommandHandler
        {
            public ParallelDispatcher.Handler handler;
            AssetID id;
            public string Name { get { return id.path; } }
            public ELoadResult LoadResult { get { return ELoadResult.Success; } }
            public Object Asset { get { return null; } }
            public bool IsReferenced { get { return true; } }
            public int Id { get { return id.id; } }

            public AbHandler(AssetID id)
            {
                this.id = id;
            }

            public ParallelDispatcher.Handler GetHandler()
            {
                return handler;
            }

            public void Release()
            {
            }

            public void Retain()
            {
            }
        }


        class Asset<T> : IAssetCache<T>, ICommandHandler where T : Object
        {
            ELoadResult result;
            public static int Identify(AssetID path)
            {
                return MathExt.Hash(typeof(T).GetHashCode(), path.id);
            }
            public string text { get; private set; }
            public byte[] bytes { get; private set; }
            InnerAssetsUtil util;
            public T asset;
            public uint refers;
            public AssetHandler<T> handler;
            public ErrorHandler errHandler;
            AssetID mId;
            string mPath;
            public EAssetQoS QoS { get; set; }
            public string Name { get { return mId.path; } }
            public int Id { get; private set; }
            public bool IsReferenced => true;
            ELoadResult IAssetPtr.LoadResult { get { return result; } }
            Object IAssetPtr.Asset { get { return asset; } }
            public T AssetInstance { get { return asset; } }

            internal ParallelDispatcher.Handler mExecuting;

            internal Asset(InnerAssetsUtil util, int id, AssetID path)
            {
                this.util = util;
                Id = id;
                mId = path;
            }

            public void Regist(AssetHandler<T> handler, ErrorHandler errHandler)
            {
                if (handler != null)
                    this.handler += handler;
                if (errHandler != null)
                    this.errHandler += errHandler;
                if (result != ELoadResult.Unload && result != ELoadResult.Loading)
                {
                    if (MainThread.IsMainThread)
                        NotifyResult();
                    else
                        mExecuting = ParallelUtils.Schedule(NotifyResult, true);//  MainThread.RunOnMainThread(NotifyResult);
                }
            }

            public void Unregist(AssetHandler<T> handler, ErrorHandler errHandler)
            {
                if (handler != null)
                    this.handler -= handler;
                if (errHandler != null)
                    this.errHandler -= errHandler;
            }

            void NotifyResult()
            {
                var call = handler;
                var err = errHandler;
                handler = null;
                errHandler = null;
                if (result == ELoadResult.Success)
                {
                    call?.Invoke(this, asset);
                }
                else if (result == ELoadResult.Faild)
                {
                    var info = string.Format("load \"{0}\" error.", mPath);
                    err?.Invoke(this, info);
                    if (err == null && call != null)
                        RTLog.LogError(LogCat.Asset, info);
                }
            }

            public ParallelDispatcher.Handler GetHandler()
            {
                return mExecuting;
            }

            public void Release()
            {
                if (refers == 0)
                    throw new System.Exception(string.Format("assets \"{0}\" was already released.", Name));
                refers--;
                if (refers == 0)
                {
                    result = ELoadResult.Unload;
                    if (MainThread.IsMainThread)
                        Unload();
                    else
                        MainThread.RunOnMainThread(Unload);
                }
            }

            public void Retain()
            {
                refers++;
            }

            void Unload()
            {
                result = ELoadResult.Unload;
                if (mIsBuildingAsset)
                {
                    Debug.LogError($"It's building assets, don't unload asset right now.");
                    return;
                }
                if (asset != null && !(asset is GameObject))
                {
                    Resources.UnloadAsset(asset);
                }
                asset = null;
            }

            void Load(bool async)
            {
#if UNITY_EDITOR
                Profiler.BeginSample(async ? "async load asset" : "load asset");
                if (sBundle != null)
                {
                    mPath = sBundle.GetVariantAsset(mId.path);
                    if (RTLog.UseCategory(LogCat.Asset) && !mPath.EqualsIgnoreCase(mId.path))
                        RTLog.Log(LogCat.Asset, $"will load \"{mId.path}\" @{mPath}");
                }
                else
                {
                    mPath = mId.path;
                }
                if (mId.useAb)
                {
                    asset = AssetDatabase.LoadAssetAtPath<T>(mPath);
                }
                else
                {
                    asset = Resources.Load<T>(mPath);
                }
                lock (_lock)
                {
                    result = asset == null ? ELoadResult.Faild : ELoadResult.Success;
                }
                if (result == ELoadResult.Success && asset is TextAsset textAsset)
                {
                    if (mPath.EndsWithIgnoreCase(".bytes"))
                        bytes = textAsset.bytes;
                    else
                        text = textAsset.text;
                }
                if (asset != null && result == ELoadResult.Success)
                    util.OnLoaded(this);
                NotifyResult();
                Profiler.EndSample();
#endif
            }

#if UNITY_EDITOR
            IEnumerator ExecuteLoadAsync()
            {
                yield return InnerBundle.GetParallelHandler();
                Load(true);
            }
#endif

            public void LoadAsync()
            {
                if (mIsBuildingAsset)
                {
                    Debug.LogError($"It's building assets, don't load asset right now.");
                    return;
                }
                ELoadResult todo;
#if UNITY_EDITOR
                if (InnerBundle != null && !InnerBundle.IsActive)
                {
                    InnerBundle.Activite();
                }
#endif
                lock (_lock)
                {
                    if (result == ELoadResult.Unload)
                    {
                        result = ELoadResult.Loading;
                        todo = result;
                    }
                    else
                    {
                        todo = result == ELoadResult.Loading ? ELoadResult.Unload : result;
                    }
                }
                if (todo == ELoadResult.Loading)
                {
                    // cmd = SubThreading.Execute(GetVariant, false);
#if UNITY_EDITOR
                    mExecuting = ParallelUtils.Schedule(ExecuteLoadAsync, true, ParallelUtils.Combine(InnerBundle.GetParallelHandler(), mExecuting));
#else
                    mExecuting = ParallelUtils.Schedule(Load, true, true, mExecuting);
#endif
                    mExecuting.IsIndependent = true;
                }
            }

            public void LoadSync()
            {
                if (mIsBuildingAsset)
                {
                    Debug.LogError($"It's building assets, don't load asset right now.");
                    return;
                }
                ELoadResult todo;
                lock (_lock)
                {
                    if (result == ELoadResult.Unload)
                    {
                        result = ELoadResult.Loading;
                        todo = result;
                    }
                    else
                    {
                        todo = result == ELoadResult.Loading ? ELoadResult.Unload : result;
                    }
                }
                if (todo == ELoadResult.Loading)
                {
                    Load(false);
                }
            }

            public override string ToString()
            {
                return string.Format("asset \"{0}\"", mId.path);
            }
        }


#if UNITY_EDITOR
        static InnerAssetsUtil sInnerUtil;
        static IInnerAssetBundle sBundle;
        public static IInnerAssetBundle InnerBundle
        {
            get { return sBundle; }
            set
            {
                if (sBundle != value)
                {
                    var old = sBundle;
                    sBundle = value;
                    if (old != null)
                        old.Deactivite();
                    if (sInnerUtil != null && value != null
                        && EditorApplication.isPlayingOrWillChangePlaymode)
                    {
                        value.Activite();
                        //sInnerUtil.mPrepare = ParallelUtils.GetParallelHandler(value);
                    }
                }
            }
        }

#endif
        //ParallelDispatcher.Handler mPrepare;
        AvlTree<IAssetPtr> mCache;

        public override UnloadAction CurrentUnloadAction => UnloadAction.None;

        public InnerAssetsUtil() : base(null)
        {

        }

        ParallelDispatcher.Handler ICommandHandler.GetHandler()
        {
#if UNITY_EDITOR
            return ParallelUtils.GetParallelHandler(sBundle);
#else
            return default;
#endif
            //return mPrepare;
        }

        protected override void OnBecomeActive()
        {
            base.OnBecomeActive();
            mCache = new AvlTree<IAssetPtr>((asset) => asset.Id);
            Variants.Initialize();
#if UNITY_EDITOR
            sInnerUtil = this;
            if (sBundle != null && ParallelUnity.isPlaying)
                sBundle.Activite();
#endif
        }

        internal override ParallelDispatcher.Handler ClearVariant(SliceList<IAssetVariantGroup> groups)
        {
#if UNITY_EDITOR
            if (sBundle != null && ParallelUnity.isPlaying)
                sBundle.Activite();
#endif
            mCache.Clear();
            return default;
        }

        public override ParallelDispatcher.Handler FindAllAssets(string abname, string regex, ICollection<string> results)
        {
#if UNITY_EDITOR
            if (sBundle != null && results != null)
            {
                System.Action act = () =>
                {

                    var assets = sBundle.GetAllAssets(abname);
                    if (assets == null)
                        return;
                    var pattern = string.IsNullOrEmpty(regex) ? null : new Regex(regex, RegexOptions.IgnoreCase);
                    for (int i = 0; i < assets.Length; i++)
                    {
                        var path = assets[i];
                        if (pattern == null || pattern.IsMatch(path))
                            results.Add(path);
                    }
                };
                return ParallelUtils.Schedule(act, false);
            }
#endif
            return default;
        }

        public override bool IsAssetExists(string asset)
        {
#if UNITY_EDITOR
            if (asset.StartsWithIgnoreCase("assets/"))
                return NativeFileSystem.HasFile(asset);
            else
                return sBundle != null && sBundle.HasAssetBundle(asset);
#else
            return false;
#endif
        }

        //        T Load<T>(string assetPath) where T : Object
        //        {
        //#if UNITY_EDITOR
        //            if (StringUtil.StartWithIgnoreCase(assetPath, "assets/"))
        //                return AssetDatabase.LoadAssetAtPath<T>(assetPath);
        //            else
        //#endif
        //                return Resources.Load<T>(assetPath);
        //        }

        protected override void OnAddProcessor(ILoadedAssetProcessor proc)
        {
            base.OnAddProcessor(proc);
            if(mCache != null)
            {
                foreach(var it in mCache)
                {
                    proc.ProcessLoadedAsset(it);
                }
            }
        }

        public override T LoadAsset<T>(string assetPath)
        {
            if (typeof(T) == typeof(AssetBundle))
                return null;
            var path = AssetID.Auto(assetPath);
            var id = Asset<T>.Identify(path);
            var data = mCache.GetData(id) as Asset<T>;
            if(data == null)
            {
                data = new Asset<T>(this, id, path);
                lock (_lock)
                {
                    mCache.Add(data);
                }
            }
            if (data.asset == null)
                data.LoadSync();
            return data.asset;
        }


        public override IAssetCache<T> LoadAssetAsync<T>(string assetPath)
        {
            if (typeof(T) == typeof(AssetBundle))
            {
                return null;
            }
#if UNITY_EDITOR
            Debug.Assert(!string.IsNullOrEmpty(assetPath), "invalid asset path (empty)");
#endif

            var path = AssetID.Auto(assetPath);
            var id = Asset<T>.Identify(path);
            var data = mCache.GetData(id) as Asset<T>;
            if (data == null)
            {
                data = new Asset<T>(this, id, path);
                lock (_lock)
                {
                    mCache.Add(data);
                }
            }
            data.LoadAsync();
            return data;
        }

        public override IAssetPtr LoadAllAssets<T>(string abname, string regex, AssetHandler<T> handler, AssetHandler<AssetBundle> finalHandler, ErrorHandler errorHandler)
        {
            var ptr = new AbHandler(AssetID.ResAsset(abname));// new InnerAssetPtr(AssetID.ResAsset(abname));
            var cmd = new LoadAllAsset<T>();
            cmd.util = this;
            cmd.regex = regex;
            cmd.handler = handler;
            cmd.finalHandler = finalHandler;
            cmd.errHandler = errorHandler;
            cmd.abname = abname;
            cmd.ptr = ptr;
            ptr.handler = ParallelUtils.Schedule(cmd, false);
            return ptr;
        }

        protected override void UnloadUnusedAsset(UnloadAction action)
        {
            lock (_lock)
            {
                if (mCache != null)
                    mCache.Clear();
            }
            Resources.UnloadUnusedAssets();
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
#if UNITY_EDITOR
            if (sBundle != null)
                sBundle.Deactivite();
#endif
            lock (_lock)
            {
                if (mCache != null)
                    mCache.Clear();
            }
        }

        public static IAssetPtr LoadAllAssetsImmediate<T>(string abname, string regex, AssetHandler<T> handler,
            AssetHandler<AssetBundle> finalHandler, ErrorHandler errorHandler) where T : Object
        {
            IAssetPtr ptr = null;// new InnerAssetPtr(AssetID.ResAsset(abname));
            if (handler == null && errorHandler == null && finalHandler == null)
                return ptr;
#if UNITY_EDITOR
            if (InnerBundle != null)
            {
                if (!InnerBundle.IsActive)
                    InnerBundle.Activite();
                var bundleAssets = InnerBundle.GetAllAssets(abname);
                if (bundleAssets != null)
                {
                    //Debug.LogFormat("Load AssetBundleInfo: {0}", abname);
                    foreach (var path in bundleAssets)
                    {
                        if (!path.StartsWithIgnoreCase("assets/") || !(string.IsNullOrEmpty(regex) || Regex.IsMatch(path, regex)))
                            continue;
                        var asset = AssetDatabase.LoadAssetAtPath<T>(path);
                        if (asset != null && handler != null)
                        {
                            handler(ptr, asset);
                        }
                        else if (asset == null && errorHandler != null)
                        {
                            errorHandler(ptr, "Faild load asset: " + path);
                        }
                    }
                }
                else
                {
                    Debug.LogErrorFormat("Don't find AssetBundleInfo: {0}", abname);
                }
                finalHandler?.Invoke(ptr, null);
                return ptr;
            }
            var assets = UnityEditor.AssetDatabase.FindAssets("t:" + typeof(T).Name);
            foreach (var t in assets)
            {
                var path = UnityEditor.AssetDatabase.GUIDToAssetPath(t);
                if (!path.StartsWithIgnoreCase("assets/") || !(string.IsNullOrEmpty(regex) || Regex.IsMatch(path, regex)))
                    continue;
                var import = UnityEditor.AssetImporter.GetAtPath(path);
                var isTarget = abname.EqualsIgnoreCase(import.assetBundleName);
                string folder = NativeFileSystem.GetFolderName(path);
                while (!isTarget && !string.IsNullOrEmpty(folder))
                {
                    var fimport = UnityEditor.AssetImporter.GetAtPath(folder);
                    isTarget = abname.EqualsIgnoreCase(fimport.assetBundleName);
                    folder = NativeFileSystem.GetFolderName(folder);
                }
                if (isTarget && handler != null)
                {
                    var asset = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
                    if (asset != null)
                        handler(ptr, asset);
                    else if (errorHandler != null)
                    {
                        errorHandler(ptr, "Faild load asset: " + path);
                    }
                }
            }
#endif
            finalHandler?.Invoke(ptr, null);
            return ptr;
        }


        class LoadAllAsset<T> : IYieldCommand where T : Object
        {
            public IAssetPtr ptr;
            public InnerAssetsUtil util;
            public string abname;
            public string regex;
            public AssetHandler<T> handler;
            public ErrorHandler errHandler;
            public AssetHandler<AssetBundle> finalHandler;

            void WaitMainThread() { }

            public IEnumerator Execute()
            {
                yield return null;

#if UNITY_EDITOR
                if (InnerBundle != null)
                {
                    if (!InnerBundle.IsActive)
                        yield return ParallelUtils.Schedule(InnerBundle.Activite, true);
                    yield return null;
                    var bundleAssets = InnerBundle.GetAllAssets(abname);
                    if (bundleAssets != null)
                    {
                        var job = ParallelUtils.NewCombination(ParallelUtils.Concat("Load All assets in ", abname));
                        //Debug.LogFormat("Load AssetBundleInfo: {0}", abname);
                        foreach (var path in bundleAssets)
                        {
                            if (!path.StartsWithIgnoreCase("assets/") ||
                                !(string.IsNullOrEmpty(regex) || Regex.IsMatch(path, regex, RegexOptions.IgnoreCase)))
                                continue;
                            var asset = util.LoadAssetAsync<T>(path) as Asset<T>;
                            asset.Regist(handler, errHandler);
                            job.Combine(asset.mExecuting);
                        }
                        job.Combine(ParallelUtils.Schedule(WaitMainThread, true));
                        yield return job;
                    }
                    else
                    {
                        Debug.LogErrorFormat("Don't find AssetBundleInfo: {0}", abname);
                    }
                }
#endif
                if (finalHandler != null)
                    yield return ParallelUtils.Schedule(() => finalHandler(ptr, null), true);
                //finalHandler?.Invoke(ptr, null);
            }
        }
    }
}